/**
 * 
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 *   
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 * 
 */
package org.jiopi.ibean.kernel.util;

import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.QName;
import org.dom4j.Namespace;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.SAXReader;
import org.dom4j.io.XMLWriter;
import org.jiopi.ibean.share.ShareUtil.IOUtil;

/**
 * 
 * 
 * @since 2010.5.23
 *
 */
public class XMLMerger {
	
	private final static ThreadLocal<HashMap<String,Integer>> elementCount = new ThreadLocal<HashMap<String,Integer>>();
	
	private static HashMap<String, String> namespaceMap = new HashMap<String, String>();
	static{
		namespaceMap.put( "jiopi", "http://www.jiopi.org/2010/JIOPISchema-configuration" );
	}
	private static Namespace JIOPI = new Namespace("jiopi", "http://www.jiopi.org/2010/JIOPISchema-configuration");
	
	public static void mergeXMLToStream(InputStream origin,InputStream replacement,OutputStream out) throws Exception{
		Document mergedDoc = mergeXML(origin,replacement);
		OutputFormat format = OutputFormat.createPrettyPrint();
		XMLWriter output = null;
		try {
			output = new XMLWriter(out,format);
			output.write(mergedDoc);
		} catch (UnsupportedEncodingException e) {
			
		}finally{
			output.close();
		}
		
		
		
	}
	
	public static Document mergeXML(InputStream origin,InputStream replacement) throws DocumentException{
		SAXReader reader = new SAXReader();
		reader.getDocumentFactory().setXPathNamespaceURIs( namespaceMap );
		
		Document originDoc = reader.read(origin);
		Document replacementDoc = reader.read(replacement);
		
		return mergeXML(originDoc,replacementDoc);
	}
	
	public static Document readDocumentAndCloseStream(InputStream is){
		if(is == null) return null;
		Document originDoc;
		try {
			SAXReader reader = new SAXReader();
			reader.getDocumentFactory().setXPathNamespaceURIs( namespaceMap );
			originDoc = reader.read(is);
			return originDoc;
		} catch (DocumentException e) {
		}finally{
			IOUtil.close(is);
		}
		return null;
	}
	
	/**
	 * 
b--覆盖-->a

1.克隆a
2.遍历b的元素
3.查询元素属性的 replace-attribute="attr,attr"
4.使用其他属性进行元素匹配(包括顺序一致性)
5.如果没找到,则直接将b的该元素插入夫级元素
6.找到
7.替换replace-attr的元素
8.查询元素属性的 replace-content
9.如果是替换content,则删除已存在的所有属性相同的标签(只删除一次)
10.否则,进入标签体，重新步骤2
	 * @param origin
	 * @param replacement
	 * @return
	 */
	public static Document mergeXML(Document origin,Document replacement){
		Document newDoc = (Document) origin.clone();

		Element originDoc = newDoc.getRootElement();
		Element replacementDoc = replacement.getRootElement();
		if(originDoc.getName().equals(replacementDoc.getName())){
			String replaceRoot = JIOPiAttributeValue(replacementDoc,"replace-content");
			if("true".equals(replaceRoot)){
				return replacement;
			}
			else{
				elementCount.set(new HashMap<String,Integer>());
				mergeElementAttribute(originDoc,replacementDoc);
				mergeSubElements(originDoc,replacementDoc);
			}
		}
		
		return newDoc;
	}
	
	private static String JIOPiAttributeValue(Element element,String attributeName){
		return element.attributeValue(QName.get(attributeName, JIOPI));
	}
	
	private static int getAndAddPath(String path){
		HashMap<String,Integer> countMap = elementCount.get();
		Integer now = countMap.get(path);
		if(now == null) now = 1;
		else now = now + 1;
		countMap.put(path, now);
		return now;
	}
	
	private static HashSet<String> getReplaceAttributeSet(Element element){
		return parseAttributeSet(JIOPiAttributeValue(element,"replace-attribute"));
	}
	
	private static HashSet<String> parseAttributeSet(String attributes){
		HashSet<String> replaceAttributeSet = new HashSet<String>();
		if(attributes!=null){
			String[] replaceAttributes = attributes.split(",");
			for(String attribute:replaceAttributes){
				attribute = attribute.trim();
				if(attribute.length()>0){
					replaceAttributeSet.add(attribute);
				}
			}
		}
		return replaceAttributeSet;
	}
	
	private static String getAttributeName(Attribute attribute){
		String attributeNamespacePrefix = attribute.getNamespacePrefix();
		String attributeToString = (attributeNamespacePrefix.length()==0?"":attributeNamespacePrefix+":")+attribute.getName();
		return attributeToString;
	}

	
	/**
	 * get relative path of a element
	 * 
	 * @return String
	 */
	@SuppressWarnings("unchecked")
	private static String getElementPath(Element element,HashSet<String> replaceAttributeSet){
		StringBuilder sb = new StringBuilder(element.getName());
		if(replaceAttributeSet == null) replaceAttributeSet = getReplaceAttributeSet(element);
		List<Attribute> attributes = element.attributes();
		for(Attribute attribute:attributes){
			if(!"http://www.jiopi.org/2010/JIOPISchema-configuration".equals(attribute.getNamespaceURI())){
				String attributeName = getAttributeName(attribute);
				if(!replaceAttributeSet.contains(attributeName)){
					sb.append(" ").append(getAttributeName(attribute)).append("=").append(attribute.getValue());
				}
			}
		}
		return sb.toString();
	}
	
	@SuppressWarnings("unchecked")
	public static Element findSubElement(Element parent,String path,int index,HashSet<String> replaceAttributeSet){
		Element findedElement = null;
		
		Iterator<Element> subElements = parent.elementIterator();
		int count = 0;
		while(subElements.hasNext()){
			Element subElement = subElements.next();
			String subElementPath = getElementPath(subElement,replaceAttributeSet);
			if(subElementPath.equals(path)) count++;
			if(count == index){
				findedElement = subElement;
				break;
			}
		}
		return findedElement;
	}
	
	/**
	 * merge sub
	 * 
	 * @param origin
	 * @param replacement
	 */
	@SuppressWarnings("unchecked")
	private static void mergeSubElements(Element origin,Element replacement){
		//System.out.println("replacement:"+replacement.getName());
		Iterator<Element> subElements = replacement.elementIterator();
		while(subElements.hasNext()){
			Element subElement = subElements.next();
			//System.out.println("==subElement:"+subElement.getName());
			HashSet<String> replaceAttributeSet = getReplaceAttributeSet(subElement);
			String elementPath = getElementPath(subElement,replaceAttributeSet);
			
			//check jiopi:reset
			String jiopiReset = JIOPiAttributeValue(subElement,"reset");
			if("true".equals(jiopiReset)){
				Element findElement = null;
				do{
					findElement = findSubElement(origin,elementPath,1,replaceAttributeSet);
					if(findElement!=null){
						origin.remove(findElement);
					}
				}while(findElement!=null);
			}
			
			//check jiopi:replace-content
			String jiopiReplaceContent = JIOPiAttributeValue(subElement,"replace-content");
			
			int elementPathIndex = getAndAddPath(replacement.getUniquePath()+elementPath);
			Element findElement = findSubElement(origin,elementPath,elementPathIndex,replaceAttributeSet);
			if(findElement!=null){
				mergeElementAttribute(findElement,subElement);
				if("replace".equals(jiopiReplaceContent)){
					replaceContent(findElement,subElement);
				}else if("add".equals(jiopiReplaceContent)){
					origin.add((Element)subElement.clone());
				}else{
					mergeSubElements(findElement,subElement);
				}
			}else{
				String jiopiTemplateExcept = JIOPiAttributeValue(subElement,"template-except");
				Element templateElement = null;
				if(jiopiTemplateExcept!=null){
					HashSet<String> exceptSet = parseAttributeSet(jiopiTemplateExcept);
					String templateElementPath = getElementPath(subElement,exceptSet);
					Element findedElement = findSubElement(origin,templateElementPath,1,exceptSet);
					if(findedElement!=null)templateElement = (Element)findedElement.clone();
				}
				if(templateElement==null){
					origin.add((Element)subElement.clone());
				}
				else{
					mergeElementAttribute(templateElement,subElement);
					mergeSubElements(templateElement,subElement);
					origin.add(templateElement);
				}
				
			}
		}
	}
	
	@SuppressWarnings("unchecked")
	private static void replaceContent(Element origin,Element replacement){
		//delete origin
		origin.clearContent();
		Iterator<Element> subElements = replacement.elementIterator();
		while(subElements.hasNext()){
			origin.add((Element)subElements.next().clone());
		}
	}
	
	/**
	 * replace attributes specified by jiopi:replace-attribute
	 * add attributes if not content
	 * @param origin
	 * @param replacement
	 */
	@SuppressWarnings("unchecked")
	private static void mergeElementAttribute(Element origin,Element replacement){
		HashSet<String> replaceAttributeSet = XMLMerger.getReplaceAttributeSet(replacement);
		List<Attribute> attributes = replacement.attributes();
		for(Attribute attribute:attributes){
			String name = XMLMerger.getAttributeName(attribute);
			boolean replace = false;
			if(replaceAttributeSet.contains(name)) replace = true;
			QName replacementAttributeName = attribute.getQName();
			Attribute originAttribute = origin.attribute(replacementAttributeName);
			if(originAttribute == null)origin.addAttribute(replacementAttributeName,attribute.getValue());
			else if(replace) originAttribute.setValue(attribute.getValue());
		}
	}
}
